أتقن التكرار غير المتزامن في JavaScript باستخدام حلقة 'for await...of' ومساعدات التكرار المخصصة. عزز معالجة التدفقات والبيانات بأمثلة عملية.
مساعد التكرار غير المتزامن في JavaScript: For Each - تكرار معالجة التدفقات
تعد البرمجة غير المتزامنة حجر الزاوية في تطوير JavaScript الحديث، حيث تمكّن التطبيقات من التعامل مع العمليات التي تستغرق وقتًا طويلاً دون حظر الخيط الرئيسي. توفر المكررات غير المتزامنة (Async iterators)، التي تم تقديمها في ECMAScript 2018، آلية قوية لمعالجة تدفقات البيانات بشكل غير متزامن. تتعمق هذه المقالة في مفهوم المكررات غير المتزامنة وتوضح كيفية تنفيذ دالة مساعدة 'for each' غير متزامنة لتبسيط معالجة التدفقات.
فهم المكررات غير المتزامنة
المكرر غير المتزامن هو كائن يتوافق مع واجهة AsyncIterator. وهو يعرّف دالة next() التي تعيد وعدًا (promise)، والذي يتم حله إلى كائن بخاصيتين:
value: القيمة التالية في التسلسل.done: قيمة منطقية (boolean) تشير إلى ما إذا كان المكرر قد اكتمل.
تُستخدم المكررات غير المتزامنة بشكل شائع لاستهلاك البيانات من مصادر غير متزامنة مثل تدفقات الشبكة أو أنظمة الملفات أو قواعد البيانات. توفر حلقة for await...of صيغة ملائمة للتكرار على الكائنات القابلة للتكرار غير المتزامنة.
مثال: القراءة من ملف بشكل غير متزامن
لنفترض سيناريو تحتاج فيه إلى قراءة ملف كبير سطراً بسطر دون حظر الخيط الرئيسي. يمكنك تحقيق ذلك باستخدام مكرر غير متزامن:
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function processFile(filePath) {
for await (const line of readFileLines(filePath)) {
console.log(`Line: ${line}`);
}
}
// Example usage
processFile('path/to/your/file.txt');
في هذا المثال، readFileLines هي دالة مولد غير متزامنة (async generator) تقوم بإصدار كل سطر من الملف عند قراءته. ثم تقوم دالة processFile بالتكرار على الأسطر باستخدام for await...of، ومعالجة كل سطر بشكل غير متزامن.
تنفيذ دالة مساعدة 'For Each' غير متزامنة
على الرغم من أن حلقة for await...of مفيدة، إلا أنها يمكن أن تصبح مطولة عندما تحتاج إلى إجراء عمليات معقدة على كل عنصر في التدفق. يمكن لدالة مساعدة 'for each' غير متزامنة تبسيط هذه العملية عن طريق تغليف منطق التكرار.
التنفيذ الأساسي
إليك تنفيذ أساسي لدالة 'for each' غير متزامنة:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
await callback(item);
}
}
تأخذ هذه الدالة كائنًا قابلاً للتكرار غير متزامن ودالة رد نداء (callback) كوسائط. تتكرر على الكائن القابل للتكرار باستخدام for await...of وتستدعي دالة رد النداء لكل عنصر. يجب أن تكون دالة رد النداء أيضًا غير متزامنة إذا كنت تريد انتظار اكتمالها قبل الانتقال إلى العنصر التالي.
مثال: معالجة البيانات من واجهة برمجة التطبيقات (API)
لنفترض أنك تجلب بيانات من واجهة برمجة تطبيقات تعيد تدفقًا من العناصر. يمكنك استخدام المساعد غير المتزامن 'for each' لمعالجة كل عنصر عند وصوله:
async function* fetchDataStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
// Assuming the API returns JSON chunks
const chunk = decoder.decode(value);
const items = JSON.parse(`[${chunk.replace(/\}\{/g, '},{')}]`); //Split chunks into valid json array
for(const item of items){
yield item;
}
}
} finally {
reader.releaseLock();
}
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem);
console.log('Finished processing data.');
}
// Example usage
main();
في هذا المثال، تقوم دالة fetchDataStream بجلب البيانات من واجهة برمجة التطبيقات وتصدر كل عنصر عند استلامه. تحاكي دالة processItem عملية غير متزامنة على كل عنصر. ثم يقوم المساعد asyncForEach بتبسيط منطق التكرار والمعالجة.
تحسينات واعتبارات
معالجة الأخطاء
من الضروري معالجة الأخطاء التي قد تحدث أثناء التكرار غير المتزامن. يمكنك تغليف دالة رد النداء في كتلة try...catch لالتقاط ومعالجة الاستثناءات:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
try {
await callback(item);
} catch (error) {
console.error(`Error processing item: ${item}`, error);
// You can choose to re-throw the error or continue processing
}
}
}
التحكم في التزامن
بشكل افتراضي، يقوم المساعد غير المتزامن 'for each' بمعالجة العناصر بشكل تسلسلي. إذا كنت بحاجة إلى معالجة العناصر بشكل متزامن، يمكنك استخدام مجموعة وعود (Promise pool) للحد من عدد العمليات المتزامنة:
async function asyncForEachConcurrent(iterable, callback, concurrency) {
const executing = [];
for await (const item of iterable) {
const p = callback(item).then(() => executing.splice(executing.indexOf(p), 1));
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEachConcurrent(fetchDataStream(apiUrl), processItem, 5); // Concurrency of 5
console.log('Finished processing data.');
}
في هذا المثال، تحد دالة asyncForEachConcurrent من عدد عمليات تنفيذ رد النداء المتزامنة إلى مستوى التزامن المحدد. يمكن أن يؤدي هذا إلى تحسين الأداء عند التعامل مع تدفقات كبيرة من البيانات.
الإلغاء
في بعض الحالات، قد تحتاج إلى إلغاء عملية التكرار قبل الأوان. يمكنك تحقيق ذلك باستخدام AbortController:
async function asyncForEach(iterable, callback, signal) {
for await (const item of iterable) {
if (signal && signal.aborted) {
console.log('Iteration aborted.');
return;
}
await callback(item);
}
}
async function main() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort(); // Abort after 2 seconds
}, 2000);
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem, signal);
console.log('Finished processing data.');
}
في هذا المثال، تتحقق دالة asyncForEach من خاصية signal.aborted قبل كل تكرار. إذا تم إلغاء الإشارة، يتم إيقاف التكرار.
تطبيقات العالم الحقيقي
يمكن تطبيق المكررات غير المتزامنة والمساعد غير المتزامن 'for each' على مجموعة واسعة من سيناريوهات العالم الحقيقي:
- خطوط أنابيب معالجة البيانات: معالجة مجموعات البيانات الكبيرة من قواعد البيانات أو أنظمة الملفات.
- تدفقات البيانات في الوقت الفعلي: التعامل مع البيانات من مآخذ الويب (web sockets) أو قوائم انتظار الرسائل أو شبكات الاستشعار.
- استهلاك واجهات برمجة التطبيقات: جلب ومعالجة البيانات من واجهات برمجة التطبيقات التي تعيد تدفقات من العناصر.
- معالجة الصور والفيديو: معالجة ملفات الوسائط الكبيرة على شكل أجزاء.
- تحليل السجلات: تحليل ملفات السجل الكبيرة سطراً بسطر.
مثال - بيانات الأسهم الدولية: لنفترض تطبيقًا يجلب أسعار الأسهم في الوقت الفعلي من مختلف البورصات الدولية. يمكن استخدام مكرر غير متزامن لتدفق البيانات، ويمكن لـ 'for each' غير المتزامن معالجة كل سعر، وتحديث واجهة المستخدم بأحدث الأسعار. يمكن استخدام هذا لعرض أسعار الأسهم الحالية لشركات مثل:
- تينسنت (الصين): جلب بيانات الأسهم لشركة تكنولوجيا دولية كبرى
- تاتا لخدمات الاستشارات (الهند): عرض تحديثات الأسهم من شركة رائدة في خدمات تكنولوجيا المعلومات
- سامسونج للإلكترونيات (كوريا الجنوبية): عرض أسعار الأسهم من شركة تصنيع إلكترونيات عالمية
- تويوتا موتور كوربوريشن (اليابان): مراقبة أسعار أسهم شركة تصنيع سيارات دولية
الخلاصة
توفر المكررات غير المتزامنة والمساعد غير المتزامن 'for each' طريقة قوية وأنيقة لمعالجة تدفقات البيانات بشكل غير متزامن في JavaScript. من خلال تغليف منطق التكرار، يمكنك تبسيط التعليمات البرمجية الخاصة بك، وتحسين قابلية القراءة، وتعزيز أداء تطبيقاتك. من خلال معالجة الأخطاء، والتحكم في التزامن، وتمكين الإلغاء، يمكنك إنشاء خطوط أنابيب معالجة بيانات غير متزامنة قوية وقابلة للتطوير.